~ blog contact resume github linkedin

How I deploy Nix + Docker

Written the 9 June 2024 by drawbu Back to blog

Hello everyone :)

I recently remade my 'portfolio' website for the fifth time and since I use and Nix as a package manager and to declare my developement environnements, it only made sense to me to avec a Nix flake at the root of my project. For the last year, it has become a habit that I have on every project I have so I never again have a problem with the dependencies I used in a old project.

I used Go + Templ to do a simple website and once I have been done I question how to deploy such a project. I'm a Nix advocate. I really like to have the possibility to define a declarative development environment with all the dependencies I need to build and dev a project. And of course curiosity hehe.

Using nix build

So currently I have a Nix flake with a devShell that exports go, templ and tailwind. Great.

1devShell = pkgs.mkShell {
2    packages = with pkgs; [go templ tailwindcss];
3};

How do I build my project? That's a tricky part. I several steps to achieve the build of the binary.

1templ generate
2mkdir -p /tmp/drawbu.dev
3tailwindcss -i ./assets/style.css -o /tmp/drawbu.dev/style.css
4go build -ldflags="-X 'main.assetsDir=/tmp/drawbu.dev'"

So... I need to make a script to build process easier. Let's use our Nix flake! We are gonna do a package named app so I can just nix build and all our build steps will be executed (with the advantages that has nix aka. declarative, reproducible & reliable builds).

Will just do a simple Go package and see were it goes.

1app = pkgs.buildGoModule {
2  name = "app";
3  src = ./.;
4  vendorHash = null;
5};

If you are interested, here is the finished version:

 1app = pkgs.buildGoModule {
 2  name = "app";
 3  src = ./.;
 4  vendorHash = null;
 5  ldflags = ["-X main.assetsDir=${placeholder "out"}/share/assets"];
 6  nativeBuildInputs = with pkgs; [templ tailwindcss makeWrapper];
 7  preBuild = ''
 8    templ generate
 9  '';
10  postBuild = ''
11    mkdir -p $out/share/assets
12    tailwindcss -i ./assets/style.css -o $out/share/assets/style.css
13  '';
14  postInstall = ''
15    wrapProgram $out/bin/app \
16      --prefix PATH : ${pkgs.lib.makeBinPath (with pkgs; [ git ])}
17  '';
18};

Docker

So go is a compiled language and so I can make good use of that feature and just deploy the binary, right? Yeah... no. You see in modern day world, you almost never see just a random binary running your entire website on a disant VPS. Instead, most of the time we try to define a Docker image. It brings simplicity to the deployment, because what works on docker on your machine, should usually works the same on the production machine (we ain't putting your laptop in prod).

You need to isolate your server from the rest of the machine, because if something goes bad, it only takes place in the container, and does not propagate to the other services and the rest of the network.

I made some research and came around this article that creates a Dockerfile where we use nix build to create binary a nix store, and create a development environment without the build time dependencies and only exports our app and it's run time dependencies.

That could work, I thought, but I still looked around and found a page on nix.dev that explain that we can declaratively declare Docker image with Nix! And ouptut them as a package!

You I made a second package exported by my flake called docker that uses pkgs.dockerTools.buildImage.

1docker = pkgs.dockerTools.buildImage {
2  name = "drawbu.dev";
3  tag = "latest";
4  copyToRoot = pkgs.buildEnv {
5    name = "image-root";
6    paths = [ self.packages.${system}.app ];
7  };
8  config.Cmd = ["app"];
9};

Then I ran nix build .#docker, and... voilà after a few seconds nix just ouputed me a docker image with the completed build of my app with only its run time dependencies!

I then make a GitHub workflow to build the image each time I push to the repo and publish it on ghcr.io.

Finally deploying

So now I just have to make a ultra simple docker-compose.yml file that uses my pre-build image and I'll have everything I need!

1services:
2  drawbu.dev:
3    image: ghcr.io/drawbu/drawbu.dev:latest
4    ports:
5        - "8080:8080"

That's it!

Thanks for reading me. This is one of the first time I do this, thanks you for your time, and I hope I see you around soon! :)